package myRobots;
import robocode.util.Utils;
import robocode.*;

import java.awt.*;
import java.awt.geom.Point2D;

import static java.lang.Math.signum;
import static java.lang.Math.toRadians;
import java.util.Random;

/**
 * Gandam - a robot by Dmitriy Zabranskiy
 */
public class Gandam extends AdvancedRobot
{
	private static final double RADIANS = toRadians(5);
    private boolean isAlive = true;
	private double gandamX;
    private double gandamY;
	private double enemyX = -1;
    private double enemyY = -1;
	private double enemyHeading;
	private double enemyVelocity;
	private double bulletPower = 1;
	private double RADIUS = 18 * Math.sqrt(2);
	private static Random random = new Random();
	private Intercept intercept = new Intercept();		
	
	public void run() {
		setTurnRadarRightRadians(Double.POSITIVE_INFINITY);
		while (isAlive) {
			if (enemyX > -1) {
                final double radarTurn = getRadarTurn();
                setTurnRadarRightRadians(radarTurn);
				
                final double bodyTurn = getBodyTurn();
                setTurnRightRadians(bodyTurn);

                if (getDistanceRemaining() == 0) {
                    final double distance = getDistance();
                    setAhead(distance);
                }
					
				intercept.calculate
				(
				  gandamX,
				  gandamY,
				  enemyX,
				  enemyY,
				  enemyHeading,
				  enemyVelocity,
				  bulletPower,
				  0 // Angular velocity
				);
				// Helper function that converts any angle into  
				// an angle between +180 and -180 degrees.
				double turnAngle = Utils.normalRelativeAngleDegrees(intercept.bulletHeading_deg - getGunHeading());

				// Move gun to target angle
				setTurnGunRight(turnAngle);

				if (Math.abs(turnAngle) <= intercept.angleThreshold) {
				  // Ensure that the gun is pointing at the correct angle
				  if (
					(intercept.impactPoint.x > 0) &&
				 (intercept.impactPoint.x < getBattleFieldWidth()) &&
				 (intercept.impactPoint.y > 0) &&
				 (intercept.impactPoint.y < getBattleFieldHeight())
				) {
					// Ensure that the predicted impact point is within 
					// the battlefield
					fire(bulletPower);
				  }
				}
				
            }

            execute();
		}
	}

    private double getRadarTurn() {
    	final double alphaToEnemy = angleTo(getX(), getY(), enemyX, enemyY);
    	final double sign = (alphaToEnemy != getRadarHeadingRadians()) ? signum(Utils.normalRelativeAngle(alphaToEnemy - getRadarHeadingRadians())): 1;

        return Utils.normalRelativeAngle(alphaToEnemy - getRadarHeadingRadians() + RADIANS * sign);
    }

    @Override
    public void onScannedRobot(ScannedRobotEvent event) {
        final double alphaToEnemy = getHeadingRadians() + event.getBearingRadians();

        enemyX = getX() + Math.sin(alphaToEnemy) * event.getDistance();
        enemyY = getY() + Math.cos(alphaToEnemy) * event.getDistance();
		enemyHeading = event.getHeading();
		enemyVelocity = event.getVelocity();
		gandamX = getX();
		gandamY = getY();
    }
	@Override
    public void onBulletHit(BulletHitEvent event) {
		bulletPower += 0.5;
    }
    @Override
    public void onBulletMissed(BulletMissedEvent event) {
		bulletPower = 1;
    }
    @Override
    public void onDeath(DeathEvent event) {
        isAlive = false;
    }

    /**
     * В Robocode немного извращённые углы - 0 смотрит на север и далее по часовой стрелке:
     * 90 - восток, 180 - юг, 270 - запад, 360 - север.
     * Метод вычисления угла между двумя точками.
     */
    private static double angleTo(double baseX, double baseY, double x, double y) {
        double theta = Math.asin((y - baseY) / Point2D.distance(x, y, baseX, baseY)) - Math.PI / 2;
        if (x >= baseX && theta < 0) {
            theta = -theta;
        }
        return (theta %= Math.PI * 2) >= 0 ? theta : (theta + Math.PI * 2);
    }
	private double getDistance() {
        return 200 - 400 * random.nextInt();
    }

    private double getBodyTurn() {
        final double alphaToMe = angleTo(enemyX, enemyY, getX(), getY());
        final double lateralDirection = signum((getVelocity() != 0 ? getVelocity() : 1) * Math.sin(Utils.normalRelativeAngle(getHeadingRadians() - alphaToMe)));
        final double desiredHeading = Utils.normalAbsoluteAngle(alphaToMe + Math.PI / 2 * lateralDirection);
        final double normalHeading = getVelocity() >= 0 ? getHeadingRadians() : Utils.normalAbsoluteAngle(getHeadingRadians() + Math.PI);
		
        return Utils.normalRelativeAngle(desiredHeading - normalHeading);
    }
   private class Coordinate {
		public double x = 0;
		public double y = 0;
		
		public Coordinate() {}
		public Coordinate(double x, double y) {
			this.x = x;
			this.y = y;
		}
		
		public void set(double x, double y) {
			this.x = x;
			this.y = y;
		}
   }
   public class Intercept {
	 public Coordinate impactPoint = new Coordinate(0,0);
	 public double bulletHeading_deg;

	 protected Coordinate bulletStartingPoint = new Coordinate();
	 protected Coordinate targetStartingPoint = new Coordinate();
	 public double targetHeading;
	 public double targetVelocity;
	 public double bulletPower;
	 public double angleThreshold;
	 public double distance;

	 protected double impactTime;
	 protected double angularVelocity_rad_per_sec;

	 public void calculate (
		 // Initial bullet position x coordinate 
		 double xb, 
		 // Initial bullet position y coordinate
		 double yb, 
		 // Initial target position x coordinate
		 double xt, 
		 // Initial target position y coordinate
		 double yt, 
		 // Target heading
		 double tHeading, 
		 // Target velocity
		 double vt, 
		 // Power of the bullet that we will be firing
		 double bPower, 
		 // Angular velocity of the target
		 double angularVelocity_deg_per_sec 
	)
	{
		angularVelocity_rad_per_sec = Math.toRadians(angularVelocity_deg_per_sec);

		bulletStartingPoint.set(xb, yb);
		targetStartingPoint.set(xt, yt);

		targetHeading = tHeading;
		targetVelocity = vt;
		bulletPower = bPower;
		double vb = 20-3*bulletPower;

		double dX,dY;

		// Start with initial guesses at 10 and 20 ticks
		impactTime = getImpactTime(10, 20, 0.01); 
		impactPoint = getEstimatedPosition(impactTime);

		dX = (impactPoint.x - bulletStartingPoint.x);
		dY = (impactPoint.y - bulletStartingPoint.y);

		distance = Math.sqrt(dX*dX+dY*dY);

		bulletHeading_deg = Math.toDegrees(Math.atan2(dX,dY));
		angleThreshold = Math.toDegrees(Math.atan(RADIUS/distance));
	}

	protected Coordinate getEstimatedPosition(double time) {
		double x = targetStartingPoint.x + targetVelocity * time * Math.sin(Math.toRadians(targetHeading));
		double y = targetStartingPoint.y + targetVelocity * time * Math.cos(Math.toRadians(targetHeading));
		
		return new Coordinate(x,y);
	}

	private double f(double time) {
		double vb = 20 - 3 * bulletPower;

		Coordinate targetPosition = getEstimatedPosition(time);
		double dX = (targetPosition.x - bulletStartingPoint.x);
		double dY = (targetPosition.y - bulletStartingPoint.y);

		return Math.sqrt(dX*dX + dY*dY) - vb * time;
	}

	private double getImpactTime(double t0, double t1, double accuracy) {

		double X = t1;
		double lastX = t0;
		int iterationCount = 0;
		double lastfX = f(lastX);

		while ((Math.abs(X - lastX) >= accuracy) && (iterationCount < 15)) {

			iterationCount++;
			double fX = f(X);

			if ((fX-lastfX) == 0.0) break;

			double nextX = X - fX*(X-lastX)/(fX-lastfX);
			lastX = X;
			X = nextX;
			lastfX = fX;
		}

		return X;
	}
}
}								 	 	 																																																							 	 	 																																											
												 	 	 																											